iT邦幫忙

0

[Day 22]碼錶(Stopwatch)+圈速(Lap)GUI

  • 分享至 

  • xImage
  •  

今天做一個可以實際用的碼錶工具:開始、暫停、重置、圈速紀錄(顯示單圈與累計),並支援快捷鍵。程式完全使用 Python 標準庫(Tkinter + time.perf_counter),不需要安裝額外套件。

成品功能

開始/暫停/重置
圈速(Lap):每次按下 Lap,記錄「單圈耗時」與「累計時間」
快捷鍵:Space=開始或暫停、L=圈速、R=重置
較精準的計時:用 time.perf_counter()(高解析度、適合量測時間)
UI 更新頻率約 30 FPS,讀秒滑順但不吃太多資源
Tkinter 為標準庫。Windows 與 macOS 內建;部分 Linux 需先安裝 python3-tk。

安裝與環境
Python 3.8 以上即可
無需額外安裝套件
Windows / macOS / Linux 皆可

完整程式碼(存成 stopwatch_gui.py)

import tkinter as tk
from tkinter import ttk
import time

# ---------- 工具:格式化時間 ----------
def fmt(t: float) -> str:
    # 轉成 HH:MM:SS.cc(cc=百分秒)
    t = max(0.0, t)
    h = int(t // 3600); t -= h * 3600
    m = int(t // 60);   t -= m * 60
    s = int(t)
    cs = int(round((t - s) * 100))
    if cs == 100: s, cs = s + 1, 0
    return (f"{h:02d}:{m:02d}:{s:02d}.{cs:02d}") if h > 0 else (f"{m:02d}:{s:02d}.{cs:02d}")

# ---------- 計時狀態 ----------
running = False          # 是否在跑
start_t = 0.0            # 本次開始的 perf_counter
accum = 0.0              # 暫停前的累計秒數
laps = []                # 每一圈的「累計秒數」
tick_job = None          # after 回呼 id

def now_elapsed() -> float:
    """目前累計秒數(暫停時固定,計時時以 perf_counter 動態累加)"""
    return accum + (time.perf_counter() - start_t if running else 0.0)

# ---------- 控制邏輯 ----------
def toggle_start():
    global running, start_t
    if running:
        pause()
    else:
        running = True
        start_t = time.perf_counter()
        status_var.set("計時中")
        tick()

def pause():
    global running, accum, tick_job
    if not running: return
    accum = now_elapsed()
    running = False
    status_var.set("已暫停")
    if tick_job:
        root.after_cancel(tick_job)

def reset():
    global running, accum, laps, tick_job
    if tick_job:
        root.after_cancel(tick_job)
    running = False
    accum = 0.0
    laps.clear()
    time_var.set("00:00.00")
    total_var.set("圈速:0")
    status_var.set("就緒")
    lap_list.delete(0, tk.END)

def add_lap():
    """新增一筆圈速:顯示單圈耗時與累計時間(最新圈顯示在最上方)"""
    if not running and accum == 0.0:
        return
    total = now_elapsed()
    laps.append(total)
    idx = len(laps)
    lap_time = total if idx == 1 else (total - laps[-2])
    lap_list.insert(0, f"Lap {idx:02d}  單圈 {fmt(lap_time)}   累計 {fmt(total)}")
    total_var.set(f"圈速:{idx}")

def tick():
    """每 33ms 更新一次 UI(約 30fps)"""
    global tick_job
    if not running: return
    time_var.set(fmt(now_elapsed()))
    tick_job = root.after(33, tick)

# ---------- GUI ----------
root = tk.Tk()
root.title("Stopwatch / Lap")

main = ttk.Frame(root, padding=16); main.grid()

status_var = tk.StringVar(value="就緒")
time_var = tk.StringVar(value="00:00.00")
total_var = tk.StringVar(value="圈速:0")

ttk.Label(main, textvariable=status_var).grid(row=0, column=0, sticky="w")
ttk.Label(main, textvariable=time_var, font=("Segoe UI", 36)).grid(row=1, column=0, columnspan=3, pady=6)

btns = ttk.Frame(main); btns.grid(row=2, column=0, columnspan=3, pady=8)
ttk.Button(btns, text="開始 / 暫停 (Space)", command=toggle_start).grid(row=0, column=0, padx=4)
ttk.Button(btns, text="圈速 (L)", command=add_lap).grid(row=0, column=1, padx=4)
ttk.Button(btns, text="重置 (R)", command=reset).grid(row=0, column=2, padx=4)

# 圈速清單(最新在最上方)
list_frame = ttk.Frame(main); list_frame.grid(row=3, column=0, columnspan=3, sticky="nsew")
root.rowconfigure(3, weight=1); root.columnconfigure(0, weight=1)
list_frame.rowconfigure(0, weight=1); list_frame.columnconfigure(0, weight=1)
lap_list = tk.Listbox(list_frame, height=8)
lap_list.grid(row=0, column=0, sticky="nsew")
scroll = ttk.Scrollbar(list_frame, orient="vertical", command=lap_list.yview)
scroll.grid(row=0, column=1, sticky="ns")
lap_list.configure(yscrollcommand=scroll.set)

ttk.Label(main, textvariable=total_var).grid(row=4, column=0, sticky="w", pady=(6,0))

# 快捷鍵
root.bind("<space>", lambda e: toggle_start())
root.bind("<l>", lambda e: add_lap()); root.bind("<L>", lambda e: add_lap())
root.bind("<r>", lambda e: reset());   root.bind("<R>", lambda e: reset())

root.mainloop()

實作:
https://ithelp.ithome.com.tw/upload/images/20251008/20169368QZGVsNW4Fh.png
實作重點解說
1)為什麼用 time.perf_counter()?
time.time() 會受系統時鐘調整影響,不適合做碼錶
time.perf_counter() 提供單調遞增、高解析度的計時來源,更準確
設計上不以「每秒 +1」來計時,而是讀取起點到現在的差值。因此即使 UI 更新有抖動,累計時間仍準確。
2)避免「定時器越跑越快」:記得取消 after
Tkinter 的 after 會排程下一次回呼;若在暫停或重置時沒取消,就可能疊出多條計時線。
程式在 pause()、reset() 皆會呼叫 root.after_cancel(tick_job),確保只留下唯一的定時器。
3)圈速(Lap)的計算方式
laps 儲存每次按下 Lap 時的累計秒數
單圈耗時 = 本次累計 − 上次累計
第一圈沒有上一次,直接等於本次累計
新增在 Listbox 的最上方,最新一圈容易查看
4)UI 更新頻率 33ms(約 30 FPS)
root.after(33, tick) 讓介面更新流暢,同時避免 10ms 這種過度頻繁造成 CPU 浪費
文字採「百分秒(0.01s)」顯示;就算更新頻率略抖,總秒數的計算仍由 perf_counter() 決定,不會偏差

常見問題與排錯
按開始沒反應? 檢查是否有輸入焦點在 Listbox;按一下視窗空白處或按 Space 再試。
時間顯示跳動不均? UI 更新受系統排程影響很正常;總時間不受影響。
電腦睡眠/休眠後誤差? 喚醒後 perf_counter() 會繼續走,碼錶視為「不中斷」;若要忽略睡眠時段,可在喚醒後按一次暫停再開始。

下一篇補充可以新增的功能~


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言